Automatisation des tests

Introduction

Qui suis je ?

whoami

Déroulement du cours

  • N’hésitez pas à interrompre ou à intervenir

  • Merci de ne pas faire de bruit :)

Le contrôle continu

  • Comment ? Un questionnaire à choix multiples

  • Quand ? La dernière heure avec Richard Liot

Les sources

Découvrez l’importance des tests

Je viens de rework une fonction de 900 lignes sans aucun test

ci fire

Coder c’est tester !

  • Tester ce n’est pas que vérifier que son application marche!

    • C’est savoir rapidement quand l’application ne marche plus

    • où dans le code

    • et pourquoi

  • Construire sa couverture de tests = construire ses TNR (Tests de Non Régression)

Apprendre à coder c’est apprendre à tester

  • Impact sur la conception

  • Modularité & testabilité

XP pour eXtreme Programming

  • La méthodologie eXtreme Programming est une méthode de gestion de projet qui applique à l’extrême les principes de ceux des méthodes Agiles.

    • on se concentre sur les besoins du client ;

    • mise en place d’un développement itératif (sprints courts de 2/3 semaines) et de l’intégration continue.

  • La méthode XP s’appuie sur :

    • une forte réactivité au changement des besoins du client ;

    • un travail d’équipe ;

    • la qualité du travail fourni ;

    • la qualité des tests effectués au plus tôt.

TDD pour Test Driven Development

  • Il s’agit d’une technique de conception où le programmeur écrit d’abord le test avant de produire le moindre code.

  • Le développeur écrit ensuite le code pour que le test passe.

  • Une fois son test finalisé, il pourra être libre de refactorer autant qu’il le souhaite jusqu’à obtenir un code « propre ».

  • C’est une idée simple mais complexe à mettre en oeuvre.

  • Courbe d’apprentissage plus lente de prime abord.

Pourquoi ces pratiques ?

  • Vérifier la bonne compréhension des fonctionnalités

  • Meilleure couverture de tests automatisés

  • Facilité d’écriture des tests avant le code « métier »

  • Ils servent à promouvoir et vérifier la qualité et la fiabilité du code

    • Enfin, jusqu’à une certaine limite !!

dilbert tdd

Comment appliquer XP et TDD ?

  • Ecriture d’un test pour une fonctionnalité

  • Le test est « failed »

  • Codage de la fonctionnalité minimale

  • Vérification du cas passant

  • Répéter l’opération en enrichissant la fonctionnalité en refactorisant

Red / Green / Refactor

tdd

Rappels

Les tests unitaires

  • Les tests unitaires consistent à tester individuellement les composants d’une application.

  • On pourra ainsi valider la qualité du code et les performances d’un module.

Les tests d’intégration

  • Ces tests sont exécutés pour valider l’intégration des différents modules entre eux et dans leur environnement d’exploitation définitif.

  • Ils permettront de mettre en évidence des problèmes d’interfaces entre différents programmes.

Les tests fonctionnels

  • Ces tests ont pour but de vérifier la conformité de l’application développée avec le cahier des charges initial.

  • Ils sont donc basés sur les spécifications fonctionnelles et techniques.

  • L’écriture de tests fonctionnels automatisés représente un effort important.

Les tests d’acceptation

  • Que pouvez-vous accepter pour valider une fonctionnalité ?

    • Conformité des fonctionnalités demandées.

    • Les temps de réponses sont-ils corrects (chargement d’une page HTML, réponse d’une API, …​) ?

Les tests de charge et de performance

  • Ce sont des tests permettant de mesurer les temps de réponses du système en fonction des sollicitations.

  • Les tests de charge simulent un nombre prédéfini d’utilisateurs en simultané pour mesurer le dimensionnement de l’infrastructure nécessaire (serveurs, bande passante sur le réseau, …​)

  • Les tests de performance permettent de récupérer des métriques (temps de réponses, percentile)

90 percentile response time

Les tests en boîte noire, grise ou blanche

  • Les tests en « boite noire » consistent à examiner uniquement les fonctionnalités d’une application.

  • Les tests en « boîte blanche » consistent à examiner le fonctionnement d’une application et sa structure interne, ses processus, plutôt que ses fonctionnalités.

  • Les tests en « boîte grise » compilent ces deux précédentes approches : ils éprouvent à la fois les fonctionnalités et le fonctionnement d’un système.

black grey white testing

Analogie des « boîtes » en comparant le système testé à une voiture.

  • En méthode « boîte noire », on vérifie que la voiture fonctionne en allumant les lumières, en klaxonnant et en tournant la clé pour que le moteur s’allume. Si tout se passe comme prévu, la voiture fonctionne.

  • En méthode « boîte blanche », on emmène la voiture chez le garagiste, qui regarde le moteur ainsi que toutes les autres parties (mécaniques comme électriques) de la voiture. Si elle est en bon état, elle fonctionne.

  • En méthode « boîte grise », on emmène la voiture chez le garagiste, et en tournant la clé dans la serrure, on vérifie que le moteur s’allume, et le garagiste observe en même temps le moteur pour s’assurer qu’il démarre bien selon le bon processus.

Pyramide des tests

pyramide testing

Proportions des tests

reversed pyramid testing

Les frameworks de tests

Les frameworks pour les tests unitaires

  • JUnit, TestNG (java)

  • Jasmine, Karma, Mocha (javascript)

  • nUnit (.Net)

  • SimpleTest (PHP)

  • dUnit (Delphi)

  • cppUnit (C++) etc..

unit tests frameworks

Les frameworks pour le mocking

  • Mockito, EasyMock, PowerMock (java)

  • Sinon JS, Jest, Rhino Mocks (javascript)

  • TypeMock, Moq (.Net)

mock tests frameworks

Les outils de tests de charge

  • JMeter, Gatling, Taurus, Locust

performance frameworks

Les outils d’analyse de couverture de tests

  • Cobertura, JaCoCo (java)

  • Coverage.py (python)

  • Bullseye Coverage (C++, C)

  • NCover, dotCover (.Net)

coverage tests tools

Et bien d’autres

  • Tests manuels (SoapUI, Postman)

  • Tests d’API (frameworks REST Assured, Karate)

  • Outils de tests de sécurité

    • type SAST (Source Code Analysis Tools)

    • type DAST (Dynamic Application Security Testing)

  • Tests IHM (GUI testing) comme Seleniumn, QTP ou Cucumber

  • …​

L’automatisation

automation

Plateforme Intégration Continue

  • PIC : Plateforme agrégeant des outils permettant l’IC

cip

Job classique

commit build

Principaux avantages

  • Test immédiat des modifications

  • Notification rapide en cas de problèmes

  • Les problèmes d’intégration sont détectés et réparés de façon continue

Les difficultés lors de la mise en oeuvre de tests unitaires

  • réticences à la mise en oeuvre

  • difficultés de rédaction et de codage

  • couverture du code testé

  • temps nécessaire à la rédaction des cas de tests

  • véracité des cas de tests

  • temps nécessaire à la maintenance des cas de tests

  • les cas de tests doivent être répétables

  • complexité ⇒ base de données, fichiers

  • …​

JUnit 4 les annotations

JUnit 4La description

@BeforeClass

La méthode est exécutée une fois avant le début de tous les tests.

@AfterClass

La méthode est exécutée une fois tous les tests joués.

@Ignore or @Ignore("Why disabled")

Marque que le test doit être désactivé.

@Test (expected = Exception.class)

Échec si la méthode ne lance pas l’exception nommée.

@Test(timeout=100)

Échoue si la méthode prend plus de 100 millisecondes.

JUnit 4La description

import org.junit.*

Package pour l’utilisation des annotations.

@Test

Identifie une méthode en tant que méthode de test.

@Before

Exécuté avant chaque test (identifié par une méthode).

@After

Exécuté après chaque test (chaque méthode).

JUnit 4 les assertions

DéclarationLa description

fail([message])

Laissez la méthode échouer.

assertTrue([message,] boolean condition)

Vérifie que la condition booléenne est vraie.

assertFalse([message,] boolean condition)

Vérifie que la condition booléenne est fausse.

assertEquals([message,] expected, actual)

Teste que deux valeurs sont identiques.

assertEquals([message,] expected, actual, tolerance)

Vérifiez que les valeurs float ou double correspondent.

  • Besoin de plus de puissance ? utiliser AssertJ ou Hamcrest.

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.containsString;
...
assertThat("Hello world", containsString("world"));
DéclarationLa description

assertNull([message,] object)

Vérifie que l’objet est nul.

assertNotNull([message,] object)

Vérifie que l’objet n’est pas nul.

assertSame([message,] expected, actual)

Vérifie que les deux variables se réfèrent au même objet.

assertNotSame([message,] expected, actual)

Vérifie que les deux variables se réfèrent à des objets différents.

JUnit 5 - plus flexible

  • une API pour écrire des tests

  • un mécanisme pour découvrir et lancer les tests

  • une API pour lancer les tests (pour les outils comme Eclipse)

Contrairement aux versions précédentes de JUnit, JUnit 5 est composé de plusieurs modules différents issus de trois sous-projets différents :

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

Intégration dans un projet maven :

<!-- ... -->
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.8.1</version>
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
        <!-- ... -->
    </plugins>
</build>
<!-- ... -->

Exécution dans un projet maven :

Le plugin Maven Surefire permet d’exécuter l’ensemble des tests unitaires d’un projet (méthodes annotées @Test). Il recherchera les classes de tests dont les noms complets correspondent aux modèles suivants :

  • **/Test*.java

  • **/*Test.java

  • **/*Tests.java

  • **/*TestCase.java

Spring Boot 2.6 embarque directement JUnit5 par l’intermédiaire de sa dépendance spring-boot-starter-test.

1er test exemple :

Junit 4 :

import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class BasicTest {
	@Test
	public void simpleTest1() {
		LOGGER.info("--- Running test junit 4 -> 1 ---");
		assertTrue(true);
	}
}

Junit 5 :

import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

class BasicTest {
	@Test
	void simpleTest1() {
		LOGGER.info("--- Running test junit 5 -> 1 ---");
		assertTrue(true);
	}
}
Note
Une classe / méthode de test Junit 5 peut être avec une visibilité private, non nécessairement public.

Compatibilité JUnit 3/4 et JUnit 5

Il est possible dans un projet d’exécuter des tests Junit 3/4 existants et d’y ajouter des tests Junit Jupiter.

<!-- ... -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2/version>
    </dependency>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.8.1</version>
        <scope>test</scope>
    </dependency>
<!-- ... -->

Annotations

Les annotations de base se trouvent dans le package org.junit.jupiter.api du module junit-jupiter-api.

Annotation

Description

@Test

Indique qu’une méthode est une méthode de test. Contrairement à l’annotation @Test de JUnit 4, cette annotation ne déclare aucun attribut.

@ParameterizedTest

Indique qu’une méthode est un test paramétré.

@RepeatedTest

Indique qu’une méthode est un test template pour un test répété.

@TestFactory

Indique qu’une méthode est un test factory pour des tests dynamiques.

@TestTemplate

Indique qu’une méthode est un template de cas de test conçu pour être invoqué plusieurs fois en fonction du nombre de contextes d’appel.

@TestClassOrder

Utilisé pour configurer l’ordre d’exécution de la classe de test pour les classes de test @Nested dans la classe de test annotée.

@TestMethodOrder

Utilisé pour configurer l’ordre d’exécution de la méthode de test pour la classe de test annotée. similaire à @FixMethodOrder de JUnit 4.

@TestInstance

Utilisé pour configurer le cycle de vie de l’instance de test pour la classe de test annotée.

Annotation

Description

@DisplayName

Déclare un nom d’affichage personnalisé pour la classe de test ou la méthode de test.

@DisplayNameGeneration

Déclare un générateur de nom d’affichage personnalisé pour la classe de test.

@BeforeEach

Indique que la méthode annotée doit être exécutée avant chaque méthode @Test, @RepeatedTest, @ParameterizedTest ou @TestFactory dans la classe actuelle. Analogue à @Before de JUnit 4.

@AfterEach

Indique que la méthode annotée doit être exécutée après chaque méthode @Test, @RepeatedTest, @ParameterizedTest ou @TestFactory dans la classe actuelle. Analogue à @After de JUnit 4.

@BeforeAll

Indique que la méthode annotée doit être exécutée avant toutes les méthodes @Test, @RepeatedTest, @ParameterizedTest et @TestFactory dans la classe actuelle. analogue à @AfterClass de JUnit 4.

@AfterAll

Indique que la méthode annotée doit être exécutée après toutes les méthodes @Test, @RepeatedTest, @ParameterizedTest et @TestFactory dans la classe actuelle. analogue à @AfterClass de JUnit 4.

Annotation

Description

@Nested

Indique que la classe annotée est une classe de test imbriquée non statique. Les méthodes @BeforeAll et @AfterAll ne peuvent pas être utilisées directement dans une classe de test @Nested à moins que le cycle de vie de l’instance de test "par classe" ne soit utilisé.

@Tag

Utilisé pour déclarer des tags pour les tests de filtrage, que ce soit au niveau de la classe ou de la méthode. analogue aux groupes de test dans TestNG ou aux catégories dans JUnit 4.

@Disabled

Utilisé pour désactiver une classe de test ou une méthode de test. analogue à @Ignore de JUnit 4.

@Timeout

Utilisé pour faire échouer un test, une fabrique de tests, un modèle de test ou une méthode de cycle de vie si son exécution dépasse une durée donnée.

@ExtendWith

Utilisé pour enregistrer les extensions de manière déclarative.

@RegisterExtension

Utilisé pour enregistrer des extensions de manière programmative.

@TempDir

Utilisé pour fournir un répertoire temporaire via une injection de champ ou une injection de paramètres dans une méthode de cycle de vie ou une méthode de test. Situé dans le package org.junit.jupiter.api.io.

Exemple d’utilisation des annotations de base

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BasicTest {

	public static final Logger LOGGER = LoggerFactory.getLogger(BasicTest.class);

	@BeforeAll
	static void initAll() {
		LOGGER.info("--- Running once before first class test junit 5 ---");
	}

	@BeforeEach
	public void init() {
		LOGGER.info("--- Running before test junit 5 ---");
	}

	@Test
	void succeedingTest() {
		LOGGER.info("--- Running test junit 5 -> 1 ---");
		assertTrue(true);
	}

	@Test
	@Disabled("Test ne marche plus mais il faut livrer")
	void skipFailingTest() {
		LOGGER.info("--- Running test junit 5 -> 2 ---");
		fail("Failing test");
	}

	@AfterEach
	void tearDown() {
		LOGGER.info("--- Running after test junit 5 ---");
	}

	@AfterAll
	static void tearDownAll() {
		LOGGER.info("--- Running once after last class test junit 5 ---");
	}
}

Tests répétés

L’annotation @RepeatedTest permet de répéter plusieurs fois un même test.

Exemple :

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;

class RepeatedTestsDemo {

    @BeforeEach
    void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        int currentRepetition = repetitionInfo.getCurrentRepetition();
        int totalRepetitions = repetitionInfo.getTotalRepetitions();
        String methodName = testInfo.getTestMethod().get().getName();
        logger.info(String.format("About to execute repetition %d of %d for %s", //
            currentRepetition, totalRepetitions, methodName));
    }

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    }

    @RepeatedTest(5)
    void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
        assertEquals(5, repetitionInfo.getTotalRepetitions());
    }
}
/*
├─ RepeatedTestsDemo ✔
│  ├─ repeatedTest() ✔
│  │  ├─ repetition 1 of 10 ✔
│  │  ├─ repetition 2 of 10 ✔
│  │  ├─ repetition 3 of 10 ✔
│  │  ├─ repetition 4 of 10 ✔
│  │  ├─ repetition 5 of 10 ✔
│  │  ├─ repetition 6 of 10 ✔
│  │  ├─ repetition 7 of 10 ✔
│  │  ├─ repetition 8 of 10 ✔
│  │  ├─ repetition 9 of 10 ✔
│  │  └─ repetition 10 of 10 ✔
│  ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔
│  │  ├─ repetition 1 of 5 ✔
│  │  ├─ repetition 2 of 5 ✔
│  │  ├─ repetition 3 of 5 ✔
│  │  ├─ repetition 4 of 5 ✔
│  │  └─ repetition 5 of 5 ✔
*/

Cas d’usages limités : s’assurer que le résultat d’un traitement reste identique après n exécutions, qu’un traitement doit retourner une erreur au bout de n exécutions …​

Tests paramétrés

L’annotation @ParameterizedTest permet de répéter plusieurs fois un même test mais avec des paramètres différents (nécessite dépendance junit-jupiter-params). Les valeurs de paramètres sont définis par l’intermédiaire d’une annotation @*Source, plusieurs techniques permettent de les alimenter.

Exemple simple d’une liste de paramètre String :

class ParameterizedTestDemo {

    @ParameterizedTest
    @NullSource
    @EmptySource
    @ValueSource(strings = {"String 1", "String 2", "\n"})
    void shouldExecuteForStringList(String input) {
        /* input = [ null, "", "String 1", "String 2", "\n"] */
    }

}
Source paramètres @ValueSource

L’annotation @ValueSource accepte les types primitifs + java.lang.String et java.lang.Class, par exemple :

@ValueSource(ints = { 1, 2, 3 })
Source paramètres @EnumSource

L’annotation @EnumSource permet d’utiliser des constantes de type Enum, par exemple :

    @ParameterizedTest
    @EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
    void testEnumParam(Month param) {
        LOGGER.info("Exécution test enum param, valeur : " + param);
        assertNotNull(param);
    }
Source paramètres @MethodSource

L’annotation @MethodSource permet d’exécuter une méthode statique pour générer une liste de paramètres, par exemple :

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithExplicitLocalMethodSource(String argument) {
        assertNotNull(argument);
    }

    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana");
    }

    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(
            arguments("apple", 1, Arrays.asList("a", "b")),
            arguments("lemon", 2, Arrays.asList("x", "y"))
        );
    }
Source paramètres @CsvSource

L’annotation @CsvSource permet de charger des paramètres décrits sous forme csv, par exemple :

    @ParameterizedTest
    @CsvSource({
        "apple, 1",
        "banana, 2",
        "lemon, 3",
        "strawberry, 4"
    })
    void testWithCsvSource(String fruit, int rank) {
        assertNotNull(fruit);
        assertNotEquals(0, rank);
    }
Source paramètres @CsvFileSource

L’annotation @CsvFileSource permet de charger des paramètres générés à partir d’un fichier csv, par exemple :

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
    assertNotNull(country);
    assertNotEquals(0, reference);
}

Répertoire temporaire

L’annotation @TempDir permet l’utilisation d’un répertoire temporaire pour l’ensemble des tests d’une classe (Répertoire créé dans /tmp puis supprimé automatiquement à chaque test).

Avec JUnit 4 :
@Rule
public TemporaryFolder tmpFolder = new TemporaryFolder();

ou

/* Erreur d'assertion si le temporary folder ne peut être supprimé */
public TemporaryFolder folder = TemporaryFolder.builder().assureDeletion().build();

ou

@ClassRule
public static TemporaryFolder globalFolder = new TemporaryFolder();
Avec JUnit 5 :
/* Répertoire tmp de classe */
@TempDir
static Path sharedTempDir;

/* Répertoire tmp de méthode test */
@TempDir
File tempDir;

Extensions

Contrairement aux différentes annotations d’extensions dans Junit 4 (@RunWith, @Rule, @ClassRule), le modèle d’extension JUnit Jupiter se compose d’un concept unique et cohérent : l’API Extension avec l’annotation @ExtendWith.

Exemple :

@ExtendWith(MockitoExtension.class)
@ExtendWith({ a.class, b.class })
public class ExtensionTest {

    @Test
    @ExtendWith(c.class)
    void should_use_extensions() {
        ...
    }
}

@RunWith(SpringJUnit4ClassRunner.class) en JUnit 4 devient @ExtendWith(SpringExtension.class) en JUnit 5.

Exécutions conditionnelles de tests

Il est possible à l’aide d’annotations d’extension Junit5 d’activer ou de désactiver l’exécution de tests en fonction du contexte :

  • Selon le système d’exploitation (@EnabledOnOs({ OS.LINUX, OS.WINDOWS}), @DisabledOnOs(OS.MAC) …​)

  • Selon la version java (@EnabledOnJre(JRE.JAVA_8), @DisabledOnJre(JRE.JAVA_11) …​)

  • Selon la valeur d’une propriété système (@EnabledIfSystemProperty(named = "java.vm.vendor", matches = "Oracle."),@DisabledIfSystemProperty(named = "os.version", matches = ".*10.") …​)

  • Selon la valeur d’une variable d’environnement (@EnabledIfEnvironmentVariable(named = "ORACLE_HOME", matches = "/opt/oracle/product/19c/.*") …​)

  • Selon une ou des conditions custom : créer une classe implémentant org.junit.jupiter.api.extension.ExecutionCondition

Injection de paramètres

L’utilisation de l’interface ParameterResolver permet d’injecter un paramètre dans une méthode de test.

public class MyCustomParameterResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
        // Retourne true si le type de l'objet paramètre est correct
        ...
        return parameterContext.getParameter().getType() == MyCustomParameterType.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
        //Retourne l'instance d'un objet à utiliser en paramètre
        MyCustomParameterType customParam = new MyCustomParameterType();
        ...
        return customParam;
    }
}

@ExtendWith(MyCustomParameterResolver.class)
public class CustonParameterResolverTest {

    private MyCustomParameterType customParamGlobal;

    public CustonParameterResolverTest(MyCustonParameterType customParam) {
        this.customParamGlobal = customParam;
    }

    @Test
    void test(MyCustonParameterType customParam) {
        ...
    }

Assertions

JUnit Jupiter conserve de nombreuses méthodes d’assertion de JUnit 4 et en ajoute quelques-unes qui se prêtent bien à une utilisation avec les lambdas Java 8. Toutes les assertions JUnit Jupiter sont des méthodes statiques de la classe org.junit.jupiter.api.Assertions.

Assertion "groupées"

Elles permettent d’exécuter un ensemble complet d’assertions même en cas d’erreur

assertAll("Should check user admin identity",
    () -> assertEquals("admin", user.getLastName()),
    () -> assertEquals("admin", user.getFirstName()),
    () -> assertTrue(user.isAdmin())
);

Assertion "nested"

assertAll("person",
    () -> {
        String firstName = person.getFirstName();
        assertNotNull(firstName);
        // Executed only if the previous assertion is valid.
        assertAll("first name",
            () -> assertTrue(firstName.startsWith("J")),
            () -> assertTrue(firstName.endsWith("e"))
        );
    },
    () -> {
        // Grouped assertion, so processed independently
        // of results of first name assertions.
        String lastName = person.getLastName();
        assertNotNull(lastName);
        // Executed only if the previous assertion is valid.
        assertAll("last name",
            () -> assertTrue(lastName.startsWith("D")),
            () -> assertTrue(lastName.endsWith("e"))
        );
    }
);

CRAFTSMAN RECIPES : SOIGNEZ VOS TESTS UNITAIRES

  • Comment donner du sens à vos tests unitaires ?

    • En appliquant certains principes du Behavior Driven Development (BDD)

  • Pourquoi ?

    • Afin d’obtenir une classe de tests unitaires claire et maintenable.

  • Les tests doivent être

    • compréhensibles, lisibles et facilement modifiables

    • automatisables, répétables et exécutés rapidement

TP

fast typing computer

C’est à vous ;)

Comment faciliter l’écriture de tests unitaires

  • Mockito est un framework Java, permettant :

    • de mocker ou espionner des objets,

    • simuler et vérifier des comportements,

    • ou encore simplifier l’écriture de tests unitaires.

A quoi sert un mock ?

  • Exemple dans une Architecture Hexagonale sur les principes du Domain Driven Development (DDD) :

650

Domain Driven Development (DDD)

  • L’approche DDD vise, à isoler un domaine métier avec les caractéristiques suivantes:

    • Approfondissement des règles métier spécifiques en accord avec le modèle d’entreprise, la stratégie et les processus métier.

    • Isolation des autres domaines métier et des autres couches de l’architecture de l’application.

    • Modèle construit avec un couplage faible avec les autres couches de l’application.

    • Facilement maintenable, testable et versionnable.

    • Modèle conçu avec le moins de dépendances possibles avec une technologie ou un framework.

Architecture Hexagonale

« Permettre à une application d’être pilotée aussi bien par des utilisateurs que par des programmes, des tests automatisés ou des scripts batchs, et d’être développée et testée en isolation de ses éventuels systèmes d’exécution et bases de données. »

  • L’architecture hexagonale repose sur trois principes et techniques:

    • Séparer explicitement la logique métier de la partie exposition (client-side) et persistence (server-side).

    • Les dépendances partent des couches techniques (client-side / server-side) vers la couche logique métier

    • Il faut isoler les couches en utilisant des ports et des adaptateurs

Approche Behavior Driven Development (BDD)

  • En effet, il sera très intuitif d’écrire son test en suivant la notion //Given //When //Then, et nous verrons que Mockito met l’accent sur la 1ère et la 3ème notion.

Greffer Mockito sur une classe JUnit

Deux possibilités :

  • Ajouter l’annotation @RunWith comme suit :

@RunWith(MockitoJunitRunner.class)
public class MyTestClass {

}
  • Ou à l’initialisation dans la méthode d’initialisation (ici setUp())

private AutoCloseable closeable;
...


@Before
public void setUp() {
    closeable = MockitoAnnotations.openMocks(this);
}

@After
public void tearDown() throws Exception {
    closeable.close();
}
  • Il est conseillé de libérer la ressource après chaque test (voir méthode tearDown()).

Le stubbing

Mockito est capable de « stubber » (bouchonner) des classes concrètes mais aussi des interfaces.

  • On peut appeler la méthode mock(…​) sur une classe :

User user = Mockito.mock(User.class);
  • Ou placer une annotation si la variable est en instance de classe

@Mock
User user;

Définition du comportement des objets mockés ou « Stubbing »

Retour d’une valeur unique

Mockito.when(user.getLogin()).thenReturn(‘user1’);

Faire appel à la méthode d’origine

Mockito.when(user.getLogin()).thenCallRealMethod();

Levée d’exceptions

Mockito.when(user.getLogin()).thenThrow(new RuntimeException());

Espionner un objet avec @Spy

  • La différence entre @Mock et @Spy réside dans le fait que la deuxième permet d’instancier l’objet mocké; on peut ainsi effectuer un mock partiel.

  • Quand on appelle une méthode de l’objet « espionné »

    • la vraie méthode est appelée,

    • à moins qu’un comportement ai été défini.

@Spy
User user = new User(‘user1’);

user.getLogin() // retourne user1
Mockito.when(user.getPassword()).thenReturn(‘top secret’);

Vérification d’interactions

verify(user).getLogin();

// le test passe si getLogin() est appelée avant la fin du timeout (ici 100 ms)
verify(user, timeout(100)).getLogin();

// le test passe si il n'existe aucune autre interaction sur le mock (non vérifiée)
verifyNoMoreInteractions(luser);

Injection

  • Mockito permet également d’injecter des ressources (classes nécessaires au fonctionnement de l’objet mocké), en utilisant l’annotation @InjectMock.

  • L’injection des mocks dans l’objet marqué par @InjectMock se fera (par ordre de priorité) :

    • injection par le constructeur

    • injection par la méthode de type « setter »

    • injection par l’attribut (même si celui-ci est private)

TP

ouvrir le pdf tp/tp-mocks/tp-mocks.pdf

fast typing computer

C’est à vous ;)

Q&A

question comments concerns